前言
正则表达式在平时工作中非常常见,但是它的神奇对于很多程序员可能一直像魔法一样的存在,工作中用到的大部分正则都是去网上搜索得来的,再复杂一点看着文档费时费力的拼凑一下。是不是深有感触了?一次在网上看到有关正则的视频,让我收货颇多,当时认真记录了笔记和自己的感悟,也希望给更多需要的童鞋带来帮助。文章从零开始讲正则,导致有点长,可以收藏零碎时间慢慢看,认真看完绝对受益匪浅。文章首发于我的博客,以下是正文:
Regular Expression 使用单个字符串来描述、匹配一系列符合某个语法规则的字符串。说简单了就是按照某种规则去匹配符合条件的字符串。这里先推荐一个学习正则表达式的在线工具:https://regexper.com/,网站利用图像和英文解释,可以帮助我们非常形象的学习正则表达式,该工具也可以通过github下载安装到本地,例如:一个匹配日期的正则:`^\d{4}[/-]\d{2}[/-]\d{2}$`在工具中是这样显示的:
是不是非常直观~
RegExp对象
javaScript 中通过内置对象 RegExp 支持正则表达式,有两种方法实例化 RegExp 对象:
- 字面量
- 构造函数
1. 字面量
假设你需要把一句英文里面的小写is匹配成大写的 IS,你可以这样做:
1 | var reg = /\bis\b/; |
这样就把第一个英文单词’is’替换成了’IS’,假如你想把该句中所有的单词’is’都替换成’IS’,应该这样写:
1 | var reg = /\bis\b/g; |
在正则的末尾加上’g’就好,’g’表示global,是全局匹配的意思。’g’是正则表达式的一个修饰符,修饰符有:
- ‘g’: global 全文搜索,不添加,搜索到第一个停止
- ‘i’: ignore case 忽略大小写,默认大小写敏感
- ‘m’: multiple 多行搜索 ,检测字符串中的换行符,主要是影响字符串开始标识符^和结束标识符$的使用
可能你会想,为什么句子中的’This’中的is没有被匹配成功呢,这就是我们’\b’的功劳了。
‘\b’:匹配一个单词边界,也就是指单词和空格间的位置。例如,“er\b”可以匹配“never”中的“er”,但不能匹配“verb”中的“er”。
这里的正则在’is’的前后都有’\b’,这样就只能匹配单词’is’了。
构造函数
倘若你需要使用构造函数的方式实例化正则,则上面的字面量形式可以改成这样:
1 | var reg = new RegExp('\\bis\\b','g'); |
用这种方式就不需要’/‘符号开始和结尾以表示是正则了。但是里面的’\‘等特殊字符需要用’\‘转义。
‘\‘:将下一个字符标记为一个特殊字符、或一个原义字符、或一个向后引用、或一个八进制转义符。例如,“n”匹配字符“n”。“\n”匹配一个换行符。串行“\\”匹配“\”而“\(”则匹配“(”。
元字符
正则表达式由两种基本字符类型组成:
- 原义文本字符,即代表它原本含义的字符
- 元字符,元字符是在正则表达式中有特殊含义的非字母字符,例如上文提到的’\b’,表示匹配单词边界,并不是匹配’\b’,在正则中主要存在这些特殊字符:
*,+,?,$,^,.,|,\,(,),{,},[,]
字符类(字符集合)
一般情况下,正则表达式一个字符对应字符串一个字符,例如:ab\t
就是匹配字符串’ab’+’tab’
但是更多的时候,我们匹配的并不是某个字符,而是符合一系列特征的字符串。这时候,我们就可以使用元字符’[]’来构建一个简单的类,所谓类是指符合某些特性的对象,一个泛指,而不是特指某个字符,例如:表达式’[abc]’把字符a或b或c归为一类,表达式可以匹配这样的字符。
1 | var reg = /[abc]/g; |
这样我们匹配的就是不是abc这样三个字符了,而是abc中任何一个字符,这就是元字符的一个应用。
字符类取反
使用元字符’^’创建 反向类/负向类。
反向类的意思是不属于类的内容,表达式’[^abc]’表示不是字符a或b或c的内容,例如:
1 | var reg = /[^abc]/g; |
范围类
倘若我们需要用字符类匹配数字,按照前面的匹配方式,书写可能会很麻烦,需要这样:’[0123456789]’,对于 a 到 z 的字符更是如此。
为此,正则表达式给我们提供了范围类,我们可以使用[a-z]来连接两个字符,表示从a到z的任意字符,这是一个闭区间,包含 a 和 z 本身。
1 | var reg = /[a-z]/g; |
可以发现,这样就方便了许多。
此外,在’[]’组成的类的内部是可以连写的[a-zA-Z],这样就形成了大写字母小写字母完全匹配:
1 | var reg = /[a-zA-Z]/g; |
有些童鞋可能会想,我想连范围类里面的’-‘字符也一起匹配了,我们该怎么做?
其实也很简单,例如:
1 | var reg = /[0-9]/g; //这样是跟前面一样的结果,不行 |
预定义类及边界
预定义类
正则表达式提供预预定义类来匹配常见的字符类,让我们书写更方便。
字符 | 等价类 | 含义 |
---|---|---|
. | [^\r\n] | 除了回车符和换行符之外的所有字符 |
\d | [0-9] | 数字字符 |
\D | [^0-9] | 非数字字符 |
\s | [\t\n\x0B\f\r] | 空白符 |
\S | [^\t\n\x0B\f\r] | 非空白符 |
\w | [a-zA-Z_0-9] | 单词字符(字母、数字、下划线) |
\W | [^a-zA-Z_0-9] | 非单词字符 |
digit 数字 ‘\d’, space空白 ‘\s’,word字母 ‘\w’,大写取反,妈妈再也不用担心我记错了
来看一个实际的例子,匹配一个 ab+数字+任意字符 的字符串:
1 | var reg = /ab\d./; //之前我们可能会这样写:ab[0-9][^\r\n] |
边界
除了预定义类,正则表达式还提供了几个常用的边界字符。
字符 | 等价类 |
---|---|
^ | 以xxx开始 |
$ | 以xxx结束 |
\b | 单词边界 |
\B | 非单词边界 |
我们在第一个例子中用到过’\b’单词边界,这里我们做一个跟上面第一个例子相反的,只把’This’中的’is’替换为’IS’
1 | var reg = /\Bis\b/g; |
然后我们在说一下’^’和’\$’,在类’[]’中’^’表示取反,但是不在类中的时候’^’表示以xxx开始,’$’表示以xxx结束,这两个边界字符一般放在正则的开始和结束位置。
1 | //先看没加^或$的情况 |
上面的例子,如果’^’和’$’都加上的话,是匹配不成功的,因为没有符号的字符串可以匹配成功,童鞋们可以自己试试。
这里再结合多行匹配举一个例子:
1 | var reg = /^@\d./g; |
这里你会发现,并没有像我们预期的把三行中符合预期的字符都替换成功,只有第一行成功匹配替换了,这是为什么呢?
这是因为,换行符在我们看来是换了一行写而已,但是对于程序处理字符串的时候,换行符就是一个普通的字符,并不算是我们认为的新的一行。这时候我们的修饰符’m’就可以大展身手了:
1 | var reg = /^@\d./gm; |
量词
倘若我们希望匹配一个连续出现20次的数字的字符串,通过我们之前学习的知识,我们可能会写出连续20个’\d’。假如20次你还可以接受,那100次,1000次,甚至更多次,你怎么办?
为了解决这个问题,正则表达式引入了量词的概念,下面是一些量词和他们的含义:
字符 | 含义 |
---|---|
? | 出现零次或一次(最多出现一次) |
+ | 出现一次或者多次(至少出现一次) |
* | 出现零次或者多次(任意次) |
{n} | 出现n次 |
{n,m} | 出现n到m次 |
{n,} | 至少出现n次 |
我们可以拿文章开始的日期正则举个栗子:
1 | var reg = /\d{4}[/-]\d{2}[/-]\d{2}/g; |
贪婪模式
正则表达式默认是贪婪模式,即每次匹配都尽可能的匹配多的字符,直到匹配失败为止。
举个栗子:
1 | var reg = /\d{3,6}/g; |
从上面可以看出,正则表达式匹配了’123456’,而不是’123’,’1234’,’12345’,尽可能多的匹配了6次,即贪婪模式。
倘若我们希望它只匹配3次,即尽可能少的匹配,一旦匹配成功不再继续尝试,即非贪婪模式需要怎么做呢?
很简单,在量词后面加上’?’即可,我们再用刚才的例子试一下:
1 | var reg = /\d{3,6}?/g; |
分组
假如我们有这么一个场景:匹配字符串 Byron 连续出现3次的场景,根据前面所学,我们可能会这样写:Byron{3}
。
但是这样是错误的,试试你会发现只有Byronnn才能匹配成功,即最后的n重复了3次,并不能匹配整个单词重复三次的情况:
1 | var reg = /Byron{3}/g; |
那么,我们要怎么匹配Byron连续出现3次的情况呢,这时候,正则表达式的分组’()’就帮我们解决了这个问题:
1 | var reg = /(Byron){3}/g; |
或
有时候,我们可能会需要在匹配时用到或者的关系,利用之前的’[]’字符类(字符集合)可能只能匹配单个字符的或者关系,比如匹配a或b,你可以这样写:’[ab]’,但是如果你需要匹配的是一整个单词的或者关系呢,可能’[]’就不好使了。这时候,我们用’|’可以达到或的效果:
1 | //匹配单词Byron或者Casper |
反向引用
假如我们有这样一个需求:把日期’2015-12-25’替换成’12/25/2015’,如果是你,你现在会怎么做呢?
你可能会这样写:
1 | var reg = /\d{4}-\d{2}-\d{2}/g; |
但是上面这样的写法,你只能够匹配到’2015-12-25’了,不能再匹配别的日期了,’2015-12-25’是会变的,这样就达不到需求了。
这时候,正则的反向引用就可以取到作用了。表达式在匹配时,表达式引擎会将小括号 “( )” 包含的表达式所匹配到的字符串记录(分组捕获)下来。在获取匹配结果的时候,小括号包含的表达式所匹配到的字符串可以单独获取。
在js中正则匹配成功的字符串可以用$1表示第一次匹配成功,$3表示第三次匹配成功的字符,以此类推至$99)。于是,上面的例子就可以这样写了:
1 | var reg = /(\d{4})-(\d{2})-(\d{2})/g; |
忽略分组
在上面的反向引用中,我们默认是根据’()’全部捕获记录为$1~$99的,倘若我们想忽略某个捕获要怎么办呢?
不希望捕获某些分组,只需要在分组内加上’?:’就可以了。
1 | var reg = /(?:Byron)(\d{4})-(\d{2})-(\d{2})/g; |
这时候的$1不是Byron,而是2016了。
前瞻
正则表达式从文本头部向尾部开始解析,文本尾部方向称为“前”,前瞻就是在正则表达式匹配到规则的时候,向前检查是否符合断言,后顾/后瞻方向相反。
符合和不符合断言称为肯定/正向匹配和否定/负向匹配。
上面是前瞻的概念,是不是看完有点晕?我看完也有点晕…我来解释一下:假如你需要匹配一个名字叫“张三”的人,以前我们可能是从一堆人中找出名字是“张三”的揪出来就行了,但是前瞻就是要求你,揪出的人的名字叫“张三”还不够,还必须“张三”的父亲必须叫“张二”或者其它特定条件,这样就是前瞻了,类似的,后瞻就是名字是张三还不够,儿子还必须叫“小张”等。
是不是有点明白了?不过需要注意的是,在javascript中是不支持后顾/后瞻的,所以我们也不需要关心了。(这里纠正一下,在 ES2018(ES9) 中已经支持后瞻和命名分组了)
至于符合/不符合断言,可以解释为:比如匹配要求名字叫“张三”,并且呢他父亲不叫“张二”,对于符合的我们就叫正向/肯定匹配,不符合的就叫负向/否定匹配。
我们再用表格说明一下:
名称 | 正则 | 含义 |
---|---|---|
正向前瞻 | exp(?=assert) | 我们匹配符合了exp部分的表达式,然后还不算完,必须也匹配断言部分(’()’内部,’=’后面的正则),才算成功 |
负向前瞻 | exp(?!assert) | 我们匹配符合了exp部分的表达式,然后还不算完,必须也匹配断言部分(’()’内部,’!’后面的正则),才算成功 |
正向后顾 | exp(?<=assert) | javascript不支持 |
负向后顾 | exp(?<!assert) | javascript不支持 |
现在是不是清楚了?如果还不够明白,没事,我们再举个栗子:
1 | var reg = /\w(?=\d)/g; |
需要注意,我们断言里面内容只是作为匹配的条件之一,也是必须的条件,但是匹配的本质只匹配”()”前面的正则,所以上面的结果为:’X23’,而不是’X\3’。如果要匹配结果为后者,我们按原来的写法就行了var reg = /\w\d/g;
,不是吗?
对象属性
我们在用正则表达式相关的方法时,经常会用到正则表达式相关的一些对象属性,下面我们总结一下正则表达式相关的对象属性:
- golbal: 是否全文搜索,默认false
- ignore case: 是否大小写敏感,默认false
- multiline: 多行搜索,默认false
- lastIndex: 是当前表达式匹配内容的最后一个字符的下一个位置
- source: 正则表达式的文本字符串
其中前面三个我们在上文中已经提到过了,source的话,我们一起结合起来看看代码:
1 | var reg1 = /\w/; |
golbal、ignore case、multiline默认都是false,而source就是你写的正则字符串文本了。
至于lastIndex,我们先来看一个例子:
1 | var reg1 = /\w/; |
对于上面的结果,是不是很奇怪?这就是’lastIndex’作用的结果了,(至于test方法,不懂的童鞋可以往下翻一番)。我们来输出’lastIndex’试试:
1 | var reg2 = /\w/g; |
可以发现,’lastIndex’是在不断发生变化的,即当前匹配结果的最后一个字符的下一个位置,这里第一次匹配到’a’字符,匹配结果的最后一位字符也是’a’,’a’字符的下一个位置的index就是1了。类似的第二次匹配’b’,匹配结果的最后一位字符也是’b’,’b’字符的下一个位置的index就是2。现在再看看概念,是不是明白了许多?
所以说,正则每次匹配并不是从头开始的,而是从上次的结果往后找,看看后面还有没有,有的话继续匹配,当然这必须是在’g’全局匹配的基础上,不然每次匹配都是从头开始的。
正则表达式RegExp对象本身的方法
RegExp对象自带的方法总共有三个:
- test: 检索字符串中指定的值。返回 true 或 false
- exec: 检索字符串中指定的值。返回找到的值,并确定其位置
- compile: 编译正则表达式(不常用)
test方法
test() 方法用于测试字符串参数中是否存在匹配正则表达式模式的字符串,如果存在则返回 true ,否则返回 false 。
语法为RegExpObject.test(string)
,如果字符串 string 中含有与 RegExpObject 匹配的文本,则返回 true,否则返回 false。
举个栗子:
1 | var str = "good good study, day day up"; |
exec方法
exec() 方法用于使用正则表达式模式对字符串执行搜索,并将更新全局 RegExp 对象的属性以反映匹配结果。
语法为RegExpObject.exec(string)
,如果字符串 string 中含有与 RegExpObject 匹配的文本,则返回一个数组,其中存放匹配的结果。如果未找到匹配,则返回值为 null。
并且,数组存在两个额外的属性:
- index: 声明匹配文本的第一个字符的位置
- input: 存放被检索的字符串 String
exec()方法比较复杂,全局调用和非全局调用的结果不同:
非全局(即不带’g’)调用:
- 调用非全局的 RegExp 对象的exec()时,返回数组
- 数组中第一个元素是正则表达式匹配的文本
- 数组中第二个元素是与 RegExpObject 的第一个子表达式相匹配的文本(如果有的话)
- 数组中第三个元素是与 RegExp 对象的第二个子表达式相匹配的文本(如果有的话,以此类推
看起来是不是又有点晕?不怕,我们来看看例子:
1 | var reg3 = /\d(\w)\d/; |
即输出结果为: lastIndex(这里为0是因为非全局匹配下lastIndex其实是不生效的),匹配文本的第一个字符的位置(额外属性index,这里第一个字符为’1’)和匹配结果数组(‘1a2’为匹配文本,’a’为子表达式’(\w)’的匹配结果,这里不存在第二个子表达式’()’,所以数组里面没有第三个匹配结果的值)。是不是清楚了许多?
全局调用:
- exec() 会在 RegExpObject 的 lastIndex 属性指定的字符处开始检索字符串 string。
- 当 exec() 找到了与表达式相匹配的文本时,在匹配后,它将把 RegExpObject 的 lastIndex 属性设置为匹配文本的最后一个字符的下一个位置。
- 这就是说,你可以通过反复调用 exec() 方法来遍历字符串中的所有匹配文本。
- 当 exec() 再也找不到匹配的文本时,它将返回 null,并把 lastIndex 属性重置为 0。
- 注意:如果在一个字符串中完成了一次模式匹配之后要开始检索新的字符串,就必须手动地把 lastIndex 属性重置为 0。
我们同样来举个栗子:
1 | var reg4 = /\d(\w)(\w)\d/g; |
这里我就不在解释结果的含义了,相信童鞋们看完上面的非全局的解释就可以理解这里全局的情况了。
compile方法
compile() 方法用于在脚本执行过程中编译正则表达式,也可用于改变和重新编译正则表达式。这个我们在平时工作中一般不会用到(我至今没看到过…);
它的语法为:RegExpObject.compile(regexp,modifier)
,其中参数’regexp’为正则表达式,参数’modifier’为规定匹配的类型。”g” 用于全局匹配,”i” 用于区分大小写,”gi” 用于全局区分大小写的匹配(摘录于W3C)。
我们来看一个例子:
1 | var str = "Every man in the world! Every woman on earth!"; |
上面的意思就是:在字符串中全局搜索 “man”,并用 “person” 替换。然后通过 compile() 方法,改变正则表达式reg为reg2,并继续利用正则改变后的正则表达式reg,用 “person” 替换 “man” 或 “woman”。
至于为什么用compile动态改正则,那跟新建有啥区别呢?
我查了查资料是这么说的:如果指定的正则表达式需要多次重复使用,那么编译正则表达式将会提高代码的执行效率,不过如果仅仅执行一次或者少数几次,那么将不会有明显的效果,compile提高了正则表达式的适应性!
支持正则表达式的 String 对象的方法
支持正则表达式的 String 对象的方法有:
- search: 检索与正则表达式相匹配的值
- match: 找到一个或多个正则表达式的匹配。
- replace: 替换与正则表达式匹配的子串。
- split: 把字符串分割为字符串数组。
search
search() 方法用于检索字符串中指定的子字符串,或检索与正则表达式匹配的子字符串
语法为stringObject.search(regexp)
,结果返回 stringObject 中第一个与 regexp 相匹配的子串的起始位置index,如果没有找到任何匹配的子串,则返回 -1。
需要注意的是,search() 方法不执行全局匹配,它将忽略修饰符’g’,并且总是从字符串的开始进行检索。
来看一个例子:
1 | var str = 'a1b2c3d4'; |
match
match() 方法将检索字符串,以找到一个或多个与 RegExp 匹配的文本,在 RegExp 是否有修饰符’g’影响很大。该方法类似 indexOf() 和 lastIndexOf(),但是它返回指定的值,而不是字符串的位置。
语法为stringObject.match(searchvalue)或stringObject.match(regexp)
,结果返回存放匹配结果的数组。该数组的内容依赖于 regexp 是否具有全局标志 g。
match() 方法也分全局调用和非全局调用:
非全局调用
- 如果 regexp 没有标志 g ,那么 match() 方法就只能在字符串中执行匹配一次
- 如果没有找到任何匹配文本,返回 null
- 否则它将返回一个数组,其中存放了与它找到的匹配文本有关的信息:
- 数组的第 0 个元素存放的是匹配文本,而其余的元素存放的是与正则表达式的子表达式匹配的文本。(这里与前面说的exec()方法类似)
并且,数组也存在两个额外的属性(与 exec() 方法基本相同):
- index: 声明匹配文本的第一个字符的位置
- input: 声明对 stringObject 的引用
下面是举例:
1 | var reg3 = /\d(\w)\d/; |
可以看到结果都与 exec() 方法一样,只是字符串和正则的位置交换了一下。
全局调用
全局调用就和 exec() 不同了:
- 如果 regexp 具有标志 g 则 match() 方法将执行全局检索,找到字符串中所有匹配的子字符串
- 没有找到任何匹配的子字符串则返回 null
- 如果找到了一个或者多个匹配字串,则返回一个数组
- 数组元素中存放的是字符串中所有匹配的字串,而且也没有index属性和input属性
简单的说,就是返回一个数组,数组中放着所有匹配结果。
1 | var reg4 = /\d(\w)(\w)\d/g; |
可以看出,match() 方法功能没有 exec() 方法返回那么多各种信息,但是如果只要结果数组,match() 方法效率会高一些。
split
对于split()方法我就不详细说明了,我们经常用它把字符串分割为数组。
1 | var str = 'a,b,c,d'; |
但是你可能不知道,我们在一些复杂情况下我们可以使用正则表达式解决
1 | var str = 'a1b2c3d'; |
上面可能还看不出 spilt() 方法用正则分割的有点,但是如果复杂一点的分割呢,比如:
1 | var str = 'a1b&c|d&e'; |
这样是不是看出了用正则的优势呢?
小知识:其实,我们在用 split() 分割字符’,’的时候,split() 方法也是把’,’ 隐士转换成正则’/,/‘的, search() 方法和 replace() 方法也是一样的。
replace
replace() 方法用于在字符串中用一些字符替换另一些字符,或替换一个与正则表达式匹配的子串。
语法为stringObject.replace(regexp/substr,replacement)
,结果返回一个新的字符串,是用 replacement 替换了 regexp 的第一次匹配或所有匹配之后得到的。
对于 replace() 方法,它有三种使用方式:
- String.prototype.replace(str,replaceStr);
- String.prototype.replace(reg,replaceStr);
- String.prototype.replace(reg,function);
1和2两种的使用我就不再多举例了,相信如果你认真看完前面的文章,肯定最熟悉的就是 replace() 方法了的一、二两种用法了。 这里就提一下第3种使用方法。
先说一下String.prototype.replace(reg,function);
中 function 的参数含义,function 会在每次匹配替换的时候调用,有四个参数(第二个参数不固定):
- 匹配字符串
- 正则表达式的分组内容,没有分组则没有该参数、
- 匹配项在字符串中的 index
- 原字符串
照例来举两个个栗子看看:
1 | var str = 'a1b2c3d4e5'; |
文章到这里就全部结束了,每个例子都是实际测试输出结果,码了好久好久……希望文章对您有帮助,我就心满意足了~~
完~